查看原文
其他

LightEventBus-轻量高效的事件总线

呼啸长风 郭霖
2024-07-19


/   今日科技快讯   /

近日,“Make 2024钉钉生态大会”上,钉钉宣布对所有大模型厂商开放,构建中国最开放AI生态。除了通义大模型外,MiniMax、月之暗面、智谱AI、猎户星空、零一万物、百川智能六家大模型厂商已经与钉钉达成合作。钉钉表示,目前,钉钉生态伙伴总数超过5600家,其中AI 生态伙伴已经超过100家;钉钉AI每天调用量超1000万次。

/   作者简介   /

本篇文章来自呼啸长风的投稿,文章主要分享了如何实现一个轻量级事件总线,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

呼啸长风的博客地址:
https://juejin.cn/user/2999123450531982/posts

/   前言   /

事件总线有多种实现,仅Android平台就有 EventBus、LiveEventBus、RxBus等多种实现。笔者之前也写过“50行代码完成事件总线”之类的实现。

最近重新研究了EventBus的源码。在整理源码的过程中,一方面觉察自己之前实现的“50行代码事件总线”确实简单了些,另一方面则觉得EventBus在性能和代码复杂度方面都有较大的改进空间。于是我参考EventBus的功能和实现,完成一个简化的版本。

LigthEventBus源码地址:
https://github.com/BillyWei01/LightEventBus

EventBus用法和源码地址:
https://juejin.cn/post/7379831020495749157

/   用法   /

LightEventBus 的实现参考了 greenrobot 的 EventBus。

为了尽量代码兼容原版 EventBus 的API, 类名沿用EventBus而不是LightEventBus;并且 register、unregister、post等方法名也沿用了 EventBus 的命名。

在使用上,LightEventBus 和 EventBus 最大的不同之处在于:

  1. 订阅方法的定义:EventBus 通过给类方法添加 @Subscribe 注解来标记“订阅方法”, 并在注解中传入参数。LightEventBus 订阅方法不需要声明为类的方法,不需要注解,只需要创建EventHandler实例。
  2. register/unregister:EventBus 需要传入声明了订阅方法的“订阅者”对象。

LightEventBus 传入的是EventHandler的列表。例如:EventBus 的用法如下:

class Event1
class Event2

class EventHandler {
    @Subscribe
    fun onEvent1(event: Event1) {
    }

    @Subscribe(threadMode = ThreadMode.ASYNC, sticky = true, priority = 100)
    fun onEvent2(event: Event2) {
    }
}

class EventBusTest {
    private val subscriber = EventHandler()

    fun test() {
        EventBus.getDefault().register(subscriber)
        EventBus.getDefault().post(Event1())
        EventBus.getDefault().postSticky(Event2())
        EventBus.getDefault().unregister(subscriber)
    }
}

LightEventBus 的用法如下:

class Event1
class Event2

class LightEventBusTest {
    private val handlers = listOf(
        EventHandler.create<Event1> { event ->
        },
        EventHandler.create<Event2>(threadMode = ThreadMode.ASYNC, sticky = true, priority = 100) { event ->
        }
    )

    fun test() {
        EventBus.getDefault().register(handlers)
        EventBus.getDefault().post(Event1())
        EventBus.getDefault().postSticky(Event2())
        EventBus.getDefault().unregister(handlers)
    }
}

/   性能测试   /

测试方式:冷启动,记录首次结果(各阶段的耗时,时间单位:ms)

测试设备:Huawei P30 pro

测试代码:Benchmark.kt。地址如下:
https://github.com/BillyWei01/LightEventBus/blob/main/app/src/main/java/io/github/lightevent/benchmark/Benchmark.kt

下面贴一下单个事件的测试代码。

// EventBus, 通过订阅索查找方法
object IndexEventBusTest {
    fun test(): String {
        val t0 = System.nanoTime()
        val handler1 = IndexEvent1Handler()
        // 这里触发“添加索引”,涉及类加载和方法查找
        val eventBus = EventBus.builder().addIndex(AppEventBusIndex()).build()
        val t1 = System.nanoTime()
        eventBus.register(handler1)
        val t2 = System.nanoTime()
        eventBus.post(Event1())
        val t3 = System.nanoTime()
        eventBus.unregister(handler1)
        val t4 = System.nanoTime()
        return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
                "post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
    }
}

class IndexEvent1Handler {
    @Subscribe(threadMode = ThreadMode.POSTING)
    fun onEvent1(event: Event1) {
    }
}

// EventBus, 通过反射查找方法
object ReflectionEventBusTest {
    fun test(): String {
        val t0 = System.nanoTime()
        val handler1 = ReflectionEvent1Handler()
        val t1 = System.nanoTime()
        // 查找方法发生在注册阶段
        EventBus.getDefault().register(handler1)
        val t2 = System.nanoTime()
        EventBus.getDefault().post(Event1())
        val t3 = System.nanoTime()
        EventBus.getDefault().unregister(handler1)
        val t4 = System.nanoTime()
        return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
                "post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
    }
}

class ReflectionEvent1Handler {
    @Subscribe(threadMode = ThreadMode.POSTING)
    fun onEvent1(event: Event1) {
    }
}

//  LightEventBus
object LightEventTest {
    fun test() : String {
        val t0 = System.nanoTime()
        val handler1 = listOf(EventHandler.create<Event1> {  })
        val t1 = System.nanoTime()
        EventBus.getDefault().register(handler1)
        val t2 = System.nanoTime()
        EventBus.getDefault().post(Event1())
        val t3 = System.nanoTime()
        EventBus.getDefault().unregister(handler1)
        val t4 = System.nanoTime()
        return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
                "post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
    }
}

实际上测试代码是由ksp生成,可以通过配置生成的事件数量,以下是生成100个事件时的测试结果。


备注

EventBus 3 提供了通过注解处理器生成“订阅索引”来提升EventBus的“方法查找”速度。Index-EventBus EventBus使用“订阅索引”下的测试结果;Reflection-EventBus 是EventBus使用反射查找方法下的测试结果。

测试结果解析:

  • EventBus使用“订阅索引”,注册时比用反射快一些,但是准备阶段则相对耗时。
  • LightEventBus的注册阶段不需要查找方法,所以比EventBus要快。
  • LightEventBus的发送默认不使用事件继承,所以发送速度也比EventBus快。

/   实现   /

由于 LightEventBus 参考了 EventBus 的功能和实现。这里我们先简单引述一下该文章关于 EventBus 的基本实现的描述,然后再讲述一下 LightEventBus 相对 EventBus 做了哪些变更。

EventBus的基本实现

EventBus的架构如下:


public class EventBus {
    // 事件 -> 订阅方法列表
    private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;

    // 订阅者 -> 关注的事件
    private final Map<Object, List<Class<?>>> typesBySubscriber;

    public void register(Object subscriber) {
    }

    public synchronized void unregister(Object subscriber) {
    }

    public void post(Object event) {
    }
}


EventBus的主体功能包含两个容器,三个方法。

容器

  • typesBySubscriber: 订阅者 -> 关注的事件
  • subscriptionsByEventType: 事件 -> 订阅方法列表

方法

订阅 (regiester)

  • 检索订阅者的方法,查找其中添加了@Subscribe注解的方法;
  • 取方法参数的类型得到eventType,取注解参数得到threadMode,sticky等参数;
  • 将“订阅者,事件,方法,以及其他参数”记录到 subscriptionsByEventType和 typesBySubscriber两个Map中。

取消订阅(unregister)

  • 索引订阅者关注的事件列表,遍历事件,移除subscriptionsByEventType中关于此事件的订阅方法;
  • 从typesBySubscriber中移除订阅者以及所关联的事件列表。

发布(post)

  • 在subscriptionsByEventType中索引Event所关联的Subscription列表;
  • 遍历Subscription列表,执行其方法。
  • 默认情况下,启用事件继承(eventInheritance) 。

实现简化

EventBus 的源码中,查找方法花费了相当多的代码,同时拖慢EventBus的性能。虽然后来增了注解处理器来支持加速方法查找,但又会引入编译耗时和启动耗时等负面作用。如果去掉方法查找,换用其他的定义订阅方法的方式,那实现就简单很多了。

EventBus的订阅方法类:

final class Subscription {
    final Object subscriber;
    final SubscriberMethod subscriberMethod;
}    

public class SubscriberMethod {
    final Method method;
    final ThreadMode threadMode;
    final Class<?> eventType;
    final int priority;
    final boolean sticky;
}


为了简化使用,在实现上 LightEventBus 做了如下简化:

  • 订阅方法不需要定义成某个类的方法,可以一个方法接口(lambda形式)替代。
  • 弱化了订阅者的概念(去掉subscriber),注册时只需要传入方法列表,也不用考虑继承等复杂因素。

最终,LightEventBus的“订阅方法”定义如下:

// (event: T) -> Unit 翻译成Java后,是一个名为 Function1 的接口类型
typealias Action<T> = (event: T) -> Unit

class EventHandler<T>( // 对应 SubscriberMethod
    val eventType: Class<*>,
    val threadMode: ThreadMode,
    val sticky: Boolean,
    val priority: Int,
    val action: Action<T>  // 对应 Method
) {
    companion object {
        // 增加一个静态方法,方便构建实例 (Kotlin语法糖)
        inline fun <reified T> create(
            threadMode: ThreadMode = ThreadMode.POSTING,
            sticky: Boolean = false,
            priority: Int = 0,
            noinline action: Action<T>
        ): EventHandler<T> {
            return EventHandler(T::class.java, threadMode, sticky, priority, action)
        }
    }
}


因为不再使用 Method的概念,故而直接用lambda形式的接口替代原来的“方法”,并命名为Action。相应地,将“事件的处理”定义为 EventHandler (对应原版的SubscriberMethod)。

以上所述,是EventBus和LightEventBus的最大差异。此变更主要影响了两个方面:

  1. API改变了,这一点 “用法” 一章已有说明,这里不再赘述;
  2. 实现上简化了很多,不再需要“查找方法”,性能提升了不少,代码也简化了一大半。

例如:



LightEventBus 的实现只有几个文件,其中 “EventBus.kt” 三百多行代码(包含注释),其他文件几行到几十行不等。

细节处理

除了简化方法查找之外, LightEventBus 还在一些处理细节上的处理。

事件继承

所谓“事件继承”,是指 :如果方法订阅的事件类型是父类(或者接口),发布的事件类型是子类(或者实现),则方法能够收到该事件。

EventBus 实现方式是,如果eventInheritance 为true(默认为true), 则除了获取事件本身的类型以外,还会去检索事件类型的父类,以及接口。

比如说,发送一个String类型的事件,执行如下:



事件继承有时候是挺有用的特性。但大多数情况下,其实发送者是有明确的意图的,发送者只想发送确定的类型给对应订阅者。例如,登录模块会定义类似LoginEvent之类的类型, 其发送事件时,只期望订阅了LoginEvent类型的订阅者接收,而不期望被关注 Object 类型的订阅者接收。

于是,在LightEventBus的实现中,我将eventInheritance从全局变量改为post方法的参数。同时,通过方法重载,不传eventInheritance参数的post方法,默认为false;如果明确想要订阅父类类型的方法能接收到事件,则调用post(event, true)。

fun post(event: Any) {
    post(event, false)
}

fun post(event: Any, eventInheritance: Boolean) {
}

如此,大部分情况下,发布事件就不需要检索父类和接口了。

事件注册

EventBus实现如下:

public class EventBus {
    // 事件 -> 订阅方法列表
    private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;

    public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }
    }
}


其核心处理,概括而言,就是:

  1. 查找到订阅者的方法列表后,遍历订阅者方法列表;
  2. 在注册订阅方法时,根据方法的事件类型,从 subscriptionsByEventType  检索事件类型对应的方法列表;
  3. 从头开始比较,找到优先级小于当前订阅方法的位置,插入该方法的后面(使列表中的方法,按按优先级逆序排列)。

另外,EventBus保存订阅方法列表用的是:CopyOnWriteArrayList。因为register和unregister方法会更新方法列表,而post方法会查询方法列表;用CopyOnWriteArrayList可以避免遍历的过程中写入而引发ConcurrentModificationException。

LightEventBus的实现如下:

class EventBus {
    // 事件 -> 订阅者(集合)
    private val subscriptions = mutableMapOf<Class<*>, ArrayList<EventHandler<*>>>()

    // 正在发送事件的线程的数量
    private val postingCount = AtomicInteger()

    fun register(handlers: List<EventHandler<*>>) {
        synchronized(this) {
            handlers.forEach { handler ->
                val eventType = handler.eventType
                val list = subscriptions.getOrPut(eventType) { ArrayList(2) }
                // 如果没有线程正在访问方法列表,则直接添加;
                // 如果有,则执行 CopyOnWrite
                if (postingCount.get() == 0) {
                    addHandler(list, handler)
                } else {
                    subscriptions[eventType] = ArrayList(list).apply { addHandler(this, handler) }
                }
            }
        }
    }

    // 按优先级逆序排列
    private fun addHandler(list: ArrayList<EventHandler<*>>, handler: EventHandler<*>) {
        val size = list.size
        val priority = handler.priority
        // 快速判断:列表为空,或者优先级小于等于列表末尾,则直接插入列表末尾
        if (size == 0 || priority <= list[size - 1].priority) {
            list.add(handler)
        } else {
            for (i in 0..<size) {
                if (priority > list[i].priority ) {
                    list.add(i, handler)
                    return
                }
            }
            list.add(size, handler)
        }
    }
}


相比EventBus, 做了两个处理:

  • 优先级处理。由于大部分情况下,使用者不会特别去设置优先级,所有订阅方的优先级基本都是0。因此,插入列表时,可以直接和列表末尾的方法比较,如果小于或等于其优先级,则插入队列末尾。如此,就不需要遍历整个列表了。
  • CopyOnWrite LightEventBus 增加了一个 postingCount 变量,在发生事件时+1;在执行register和unregister时,如果 postingCount 为0,则说明没有任何线程在遍历订阅方法列表;这时候可以直接添加在当前的方法列表中,而不需要先Copy再Write。

/   总结   /

EventBus是比较优秀的事件通信框架,容易上手,功能丰富。在研究其源码之后,发现也有可以改进的地方。但看github上的记录,EnentBus已经有两年没有更新了,并且挂了很多issue没有处理;加上EventBus是不可能变更订阅方法的用法的, 所以我就直接创建一个Project来写了。

LightEventBus 毕竟是一个新的Project, 有不足之处,欢迎交流指正。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android 15?我想躺着
用Native方法加密APK包所有字符串,安全性拉满

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存